import binascii
import struct
import base64
import json
import os
import tkinter
from tkinter import filedialog
from Crypto.Cipher import AES


def dump(file_path):
    """
    原始项目：ncmdump
    解密文件，生成对应的通用格式音频文件
    :param file_path: 文件路径
    :return: 解密后的文件名
    """
    # 十六进制转字符串
    core_key = binascii.a2b_hex("687A4852416D736F356B496E62617857")
    meta_key = binascii.a2b_hex("2331346C6A6B5F215C5D2630553C2728")
    # 分组加密函数，如果数据的长度不是分组的整数倍，需要填充数据到分组的倍数，填充的每个字节值为填充的长度
    # 这里用来去除填充的数据
    unpad = lambda s: s[0:-(s[-1] if type(s[-1]) == int else ord(s[-1]))]  # lambda函数，传入s。ord将字符转ASCII码对应数值
    f = open(file_path, 'rb')
    header = f.read(8)  # 读取8个字节的数据（Magic Header）
    # 字符串转十六进制
    assert binascii.b2a_hex(header) == b'4354454e4644414d'  # 验证magic头
    f.seek(2, 1)  # 将指针从当前位置处（形参1），向后移动两个字节（共10字节magic头）
    key_length = f.read(4)  # AES秘钥长度，占4字节
    key_length = struct.unpack('<I', bytes(key_length))[0]  # 十六进制数据按照小端字节序(<)，无符号整型(I)数据解析。返回元组，提取数值（128）
    key_data = f.read(key_length)  # 向后读取key长度（其实就是128）的字节
    key_data_array = bytearray(key_data)  # 转换成字节数组，可以赋值0-255
    for i in range(0, len(key_data_array)):
        key_data_array[i] ^= 0x64  # 每个字节中的值与0x64进行异或
    key_data = bytes(key_data_array)  # 转换回字节序列
    cryptor = AES.new(core_key, AES.MODE_ECB)  # AES，电码本模式(ECB)
    key_data = unpad(cryptor.decrypt(key_data))[17:]  # 去除17字节的前缀
    key_length = len(key_data)  # key长度
    key_data = bytearray(key_data)  # 转字节数组
    # RC4-KSA算法生成S盒
    key_box = bytearray(range(256))  # 生成一个字节取值为0-255的字节数组，作为s盒的初值
    c = 0  # 随机搅乱
    last_byte = 0  # 上一轮的c
    key_offset = 0  # 偏移值
    for i in range(256):
        swap = key_box[i]
        c = (swap + last_byte + key_data[key_offset]) & 0xff  # & 0xff，防止数值超过255
        key_offset += 1
        if key_offset >= key_length:
            key_offset = 0
        key_box[i] = key_box[c]
        key_box[c] = swap  # 此处[i]和[c]发生交换
        last_byte = c
    meta_length = f.read(4)
    meta_length = struct.unpack('<I', bytes(meta_length))[0]  # Meta的长度
    meta_data = f.read(meta_length)
    meta_data_array = bytearray(meta_data)
    for i in range(0, len(meta_data_array)):
        meta_data_array[i] ^= 0x63
    meta_data = bytes(meta_data_array)
    meta_data = base64.b64decode(meta_data[22:])  # 去除前缀，base64解码
    cryptor = AES.new(meta_key, AES.MODE_ECB)
    meta_data = unpad(cryptor.decrypt(meta_data)).decode('utf-8')[6:]  # 去除music:前缀，获得元数据json字符串
    meta_data = json.loads(meta_data)
    crc32 = f.read(4)  # CRC校验码
    crc32 = struct.unpack('<I', bytes(crc32))[0]
    f.seek(5, 1)  # 跳过5字节
    image_size = f.read(4)
    image_size = struct.unpack('<I', bytes(image_size))[0]  # 图片大小
    image_data = f.read(image_size)  # 图片数据
    file_name = f.name.split("/")[-1].split(".ncm")[0] + '.' + meta_data['format']  # 文件名
    m = open(os.path.join(os.path.split(file_path)[0], file_name), 'wb')
    # chunk = bytearray()
    while True:
        chunk = bytearray(f.read(0x8000))
        chunk_length = len(chunk)
        if not chunk:
            break
        for i in range(1, chunk_length + 1):  # RC4-PRGA
            j = i & 0xff
            chunk[i - 1] ^= key_box[(key_box[j] + key_box[(key_box[j] + j) & 0xff]) & 0xff]
        m.write(chunk)
    m.close()
    f.close()
    return file_name


def get_file_path(dir_path):
    """
    :param dir_path: 文件所在目录
    :return: 文件路径list
    """
    file_path_list = []
    for root, dirs, items in os.walk(dir_path):
        for item in items:
            file_path = os.path.join(root, item)
            file_path = file_path.replace('\\', '/')
            suffix = os.path.splitext(file_path)[-1]
            suffix_limit = ['.ncm']  # 格式限定
            if suffix not in suffix_limit:
                continue
            file_path_list.append(file_path)
    return file_path_list


def main():
    tkinter.Tk().withdraw()  # 创建一个Tkinter.Tk()实例，隐藏
    dir_path = filedialog.askdirectory(title='选择ncm歌曲所在目录')  # 选择文件夹
    if dir_path == '':
        print("未指定路径！")
        return
    file_path_list = get_file_path(dir_path)
    total = len(file_path_list)
    print(f'----------开始转换，总计：{total}----------')
    for num, path in enumerate(file_path_list):
        try:
            file_name = dump(path)
            print(f'{num + 1}/{total}(Succeed) - {file_name}')
        except AssertionError:
            print(f'{num + 1}/{total}(Failed) - {path}')
            continue
    print('----------全部转换完成----------')


if __name__ == '__main__':
    main()
